<template>
    <div class="china-flow-map-container">
        <div class="map-header">
            <h4 class="map-title">{{ title }}</h4>
            <div class="map-controls">
                <el-button size="small" @click="resetMap">
                    <el-icon>
                        <Refresh />
                    </el-icon>
                    重置
                </el-button>
            </div>
        </div>
        <div class="map-wrapper">
            <div v-if="isLoading" class="loading-overlay">
                <el-icon class="is-loading">
                    <Loading />
                </el-icon>
                <span>加载地图中...</span>
            </div>
            <div v-else-if="loadError" class="error-overlay">地图资源加载失败</div>
            <div v-else ref="chartRef" class="china-flow-map"></div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { Refresh, Loading } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import * as dataModule from '@/assets/data'

interface FlowItem {
    from: string
    to: string
    value?: number
    color?: string
    width?: number
    opacity?: number
    arrowSymbol?: string
}

interface Props {
    title?: string
    geoJsonUrl?: string
    mapName?: string
    height?: number
    flows: FlowItem[]
    showLabels?: boolean
    dynamic?: boolean
    arrowSymbol?: string
    showArrow?: boolean
    effectPeriod?: number
    colorScheme?: string[]
    colorByValue?: boolean
    fixedWidth?: boolean
    fixedOpacity?: boolean
    widthRange?: [number, number]
    opacityRange?: [number, number]
    lineColor?: string
    fixedPointSize?: boolean
    pointSize?: number
    pointSizeRange?: [number, number]
    ripple?: boolean
    rippleBrush?: 'stroke' | 'fill'
    // 颜色插值空间：'rgb' 或 'hsv'
    colorSpace?: 'rgb' | 'hsv'
}

const props = withDefaults(defineProps<Props>(), {
    title: '中国-飞线图',
    geoJsonUrl: '',
    mapName: 'china',
    height: 500,
    showLabels: false,
    dynamic: true,
    arrowSymbol: 'arrow',
    showArrow: true,
    effectPeriod: 4,
    colorScheme: ['#00FF00', '#FFFF00', '#FF0000'],
    colorByValue: true,
    fixedWidth: false,
    fixedOpacity: true,
    widthRange: [1, 4],
    opacityRange: [0.5, 0.9],
    lineColor: '',
    fixedPointSize: false,
    pointSize: 10,
    pointSizeRange: [6, 18],
    ripple: true,
    rippleBrush: 'stroke',
    colorSpace: 'hsv'
})

const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
const isLoading = ref(false)
const mapData = ref<any>(null)
const loadError = ref(false)

const getMapDataByName = (mapName: string) => {
    const dataKey = mapName.endsWith('Json') ? mapName : `${mapName}Json`
    if (dataModule && typeof dataModule === 'object' && dataKey in dataModule) {
        return (dataModule as any)[dataKey]
    }
    if (dataModule && typeof dataModule === 'object' && mapName in dataModule) {
        return (dataModule as any)[mapName]
    }
    return null
}

const loadMapData = async () => {
    try {
        isLoading.value = true
        loadError.value = false
        if (props.geoJsonUrl) {
            const response = await fetch(props.geoJsonUrl)
            if (!response.ok) throw new Error(`HTTP ${response.status}`)
            const geoData = await response.json()
            mapData.value = geoData
            echarts.registerMap(props.mapName, geoData)
        } else {
            const geoData = getMapDataByName(props.mapName)
            if (!geoData) throw new Error(`地图数据 "${props.mapName}" 不存在`)
            mapData.value = geoData
            echarts.registerMap(props.mapName, geoData)
        }
    } catch (e) {
        console.error('加载地图数据失败:', e)
        loadError.value = true
        mapData.value = null
    } finally {
        isLoading.value = false
    }
}

// 获取地区坐标（优先 GeoJSON feature.properties.cp / center，兜底用 bbox 中心）
const getCoordByName = (name: string): [number, number] | null => {
    const features = mapData.value?.features || []
    const f = features.find((it: any) => it.properties?.name === name)
    if (!f) return null
    const propsAny = f.properties || {}
    if (Array.isArray(propsAny.cp) && propsAny.cp.length === 2) {
        return [propsAny.cp[0], propsAny.cp[1]]
    }
    if (Array.isArray(propsAny.center) && propsAny.center.length === 2) {
        return [propsAny.center[0], propsAny.center[1]]
    }
    // bbox 兜底
    const coordinates = f.geometry?.coordinates
    if (!coordinates) return null
    const flat: number[][] = []
    const walk = (arr: any) => {
        if (typeof arr[0] === 'number') {
            flat.push(arr as number[])
        } else {
            for (const sub of arr) walk(sub)
        }
    }
    walk(coordinates)
    const lons = flat.map(c => c[0])
    const lats = flat.map(c => c[1])
    const lon = (Math.min(...lons) + Math.max(...lons)) / 2
    const lat = (Math.min(...lats) + Math.max(...lats)) / 2
    return [lon, lat]
}

const getValueDomain = () => {
    const values = props.flows.map(f => f.value ?? 1)
    const min = Math.min(...values)
    const max = Math.max(...values)
    return { min, max: Math.max(min + 1, max) }
}

const mapLinear = (v: number, inMin: number, inMax: number, outMin: number, outMax: number) => {
    const t = (v - inMin) / (inMax - inMin)
    return outMin + Math.max(0, Math.min(1, t)) * (outMax - outMin)
}

const hexToRgb = (hex: string) => {
    const m = hex.replace('#', '')
    const bigint = parseInt(m.length === 3 ? m.split('').map(c => c + c).join('') : m, 16)
    const r = (bigint >> 16) & 255
    const g = (bigint >> 8) & 255
    const b = bigint & 255
    return { r, g, b }
}

const rgbToHex = (r: number, g: number, b: number) => {
    const toHex = (x: number) => x.toString(16).padStart(2, '0')
    return `#${toHex(Math.round(r))}${toHex(Math.round(g))}${toHex(Math.round(b))}`
}

const lerpColor = (c1: string, c2: string, t: number) => {
    if (props.colorSpace === 'rgb') {
        const a = hexToRgb(c1)
        const b = hexToRgb(c2)
        return rgbToHex(a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t)
    }
    // HSV 插值，色相圆滑过渡
    const toHsv = (hex: string) => {
        const { r, g, b } = hexToRgb(hex)
        const rf = r / 255, gf = g / 255, bf = b / 255
        const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf)
        const d = max - min
        let h = 0
        if (d !== 0) {
            if (max === rf) h = ((gf - bf) / d) % 6
            else if (max === gf) h = (bf - rf) / d + 2
            else h = (rf - gf) / d + 4
            h *= 60
            if (h < 0) h += 360
        }
        const s = max === 0 ? 0 : d / max
        const v = max
        return { h, s, v }
    }
    const fromHsv = (h: number, s: number, v: number) => {
        const c = v * s
        const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
        const m = v - c
        let rf = 0, gf = 0, bf = 0
        if (0 <= h && h < 60) { rf = c; gf = x; bf = 0 }
        else if (60 <= h && h < 120) { rf = x; gf = c; bf = 0 }
        else if (120 <= h && h < 180) { rf = 0; gf = c; bf = x }
        else if (180 <= h && h < 240) { rf = 0; gf = x; bf = c }
        else if (240 <= h && h < 300) { rf = x; gf = 0; bf = c }
        else { rf = c; gf = 0; bf = x }
        return rgbToHex((rf + m) * 255, (gf + m) * 255, (bf + m) * 255)
    }
    const a = toHsv(c1)
    const b = toHsv(c2)
    // 最短色相路径插值
    let dh = b.h - a.h
    if (dh > 180) dh -= 360
    if (dh < -180) dh += 360
    const h = a.h + dh * t
    const s = a.s + (b.s - a.s) * t
    const v = a.v + (b.v - a.v) * t
    return fromHsv((h + 360) % 360, s, v)
}

// 连续线性颜色映射（绿色→黄色→红色 或按给定 colorScheme 渐变）
const pickColorByValue = (v: number) => {
    if (!props.colorByValue || !props.colorScheme?.length) return props.colorScheme?.[1] || '#19D2FF'
    const scheme = props.colorScheme
    const { min, max } = getValueDomain()
    const tRaw = (v - min) / (max - min)
    const t = Math.max(0, Math.min(1, isFinite(tRaw) ? tRaw : 0))
    if (scheme.length === 1) return scheme[0]
    const segments = scheme.length - 1
    const pos = t * segments
    const i = Math.min(segments - 1, Math.max(0, Math.floor(pos)))
    const frac = pos - i
    const c1 = scheme[i]
    const c2 = scheme[i + 1]
    return lerpColor(c1, c2, frac)
}

const buildSeries = () => {
    const lineData: any[] = []
    const points: Record<string, { coord: [number, number]; valueSum: number; color?: string }> = {}
    const { min, max } = getValueDomain()

    for (const flow of props.flows) {
        const fromCoord = getCoordByName(flow.from)
        const toCoord = getCoordByName(flow.to)
        if (!fromCoord || !toCoord) continue
        const val = flow.value ?? 1
        const color = flow.color || (props.colorByValue ? pickColorByValue(val) : (props.lineColor || '#19D2FF'))
        const width = props.fixedWidth ? (flow.width ?? props.widthRange[0]) : (flow.width ?? mapLinear(val, min, max, props.widthRange[0], props.widthRange[1]))
        const opacity = props.fixedOpacity ? (flow.opacity ?? props.opacityRange[1]) : (flow.opacity ?? mapLinear(val, min, max, props.opacityRange[0], props.opacityRange[1]))
        lineData.push({
            coords: [fromCoord, toCoord],
            value: val,
            lineStyle: { color, width, opacity }
        })
        points[flow.from] = { coord: fromCoord, valueSum: (points[flow.from]?.valueSum ?? 0) + val, color: flow.color }
        points[flow.to] = { coord: toCoord, valueSum: (points[flow.to]?.valueSum ?? 0) + val, color: flow.color }
    }

    const effectScatterData = Object.entries(points).map(([name, p]) => ({
        name,
        value: [...p.coord, p.valueSum || 1],
        itemStyle: { color: p.color || '#FFB400' }
    }))

    const series: any[] = []
    series.push({
        type: 'lines',
        coordinateSystem: 'geo',
        zlevel: 2,
        effect: {
            show: !!(props.dynamic && (props.showArrow)),
            period: props.effectPeriod,
            trailLength: 0.2,
            symbol: (props.showArrow ? (props.arrowSymbol || 'arrow') : 'none'),
            symbolSize: 6
        },
        lineStyle: { curveness: 0.2 },
        data: lineData
    })

    if (props.ripple) {
        series.push({
            type: 'effectScatter',
            coordinateSystem: 'geo',
            zlevel: 3,
            rippleEffect: { brushType: props.rippleBrush },
            label: { show: props.showLabels, formatter: '{b}', position: 'right' },
            symbolSize: (val: any) => {
                if (props.fixedPointSize) return props.pointSize
                const v = val[2] || 1
                return mapLinear(v, 1, getValueDomain().max, props.pointSizeRange[0], props.pointSizeRange[1])
            },
            data: effectScatterData
        })
    } else {
        series.push({
            type: 'scatter',
            coordinateSystem: 'geo',
            zlevel: 3,
            label: { show: props.showLabels, formatter: '{b}', position: 'right' },
            symbolSize: (val: any) => {
                if (props.fixedPointSize) return props.pointSize
                const v = val[2] || 1
                return mapLinear(v, 1, getValueDomain().max, props.pointSizeRange[0], props.pointSizeRange[1])
            },
            data: effectScatterData
        })
    }

    return series
}

const initChart = async () => {
    if (!chartRef.value) return
    await loadMapData()
    if (loadError.value || !mapData.value) return

    chartInstance = echarts.init(chartRef.value)
    chartInstance.setOption({
        geo: {
            map: props.mapName,
            roam: true,
            itemStyle: {
                areaColor: '#F4F7FC',
                borderColor: '#666'
            },
            emphasis: { itemStyle: { areaColor: '#78DFFC' } },
            label: { show: false }
        },
        series: buildSeries()
    })
}

const resetMap = () => {
    if (chartInstance) {
        chartInstance.dispatchAction({ type: 'restore' })
    }
}

const resizeChart = () => {
    chartInstance?.resize()
}

watch(() => props.flows, () => {
    if (!chartInstance || loadError.value) return
    chartInstance.setOption({ series: buildSeries() })
}, { deep: true })

// 监听样式控制相关props变化，实时刷新
watch([
    () => props.dynamic,
    () => props.showArrow,
    () => props.arrowSymbol,
    () => props.effectPeriod,
    () => props.colorByValue,
    () => props.colorScheme,
    () => props.fixedWidth,
    () => props.fixedOpacity,
    () => props.widthRange,
    () => props.opacityRange,
    () => props.lineColor,
    () => props.fixedPointSize,
    () => props.pointSize,
    () => props.pointSizeRange,
    () => props.ripple,
    () => props.rippleBrush,
], () => {
    if (!chartInstance || loadError.value) return
    chartInstance.setOption({ series: buildSeries() })
}, { deep: true })

watch(() => props.mapName, async () => {
    if (!chartInstance) return
    await loadMapData()
    initChart()
})

onMounted(() => {
    nextTick(async () => {
        await initChart()
        window.addEventListener('resize', resizeChart)
    })
})

onBeforeUnmount(() => {
    chartInstance?.dispose()
    window.removeEventListener('resize', resizeChart)
})
</script>

<style scoped>
.china-flow-map-container {
    background: #fff;
    border-radius: 8px;
    border: 1px solid #e9ecef;
    overflow: hidden;
}

.map-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    background: #f8f9fa;
}

.map-title {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
}

.map-wrapper {
    position: relative;
    padding: 10px;
}

.china-flow-map {
    width: 100%;
    height: v-bind(height + 'px');
}

.loading-overlay,
.error-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.8);
    z-index: 10;
    gap: 8px;
    color: #666;
}
</style>
